package server;

//import java.io.Serializable;
//import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import spread.*;

public class ClusterManagementModule implements IClusterManagementModule {

	private Thread CMMThread;
	
	private TreeMap<Integer, TaskEntry> TaskDB;
	
	private TreeMap<Integer, MachineEntry> MachineDB;
	
	private ProxyServer Proxy;
	
	private SpreadConnection ClusterConnection;
	
	private SpreadGroup ClusterGroup;
	
	private String privateName;
	
	private final static String GroupName = "CMM_Group";
	
	private ConnectionInformation SAConnInfo;
	
	private int MachineDBLocalID;
	
	private ServerApplication SA;
	
	private AdvancedSpreadListener SpreadListener;
	
//------------------------------------------------------------------------
//  Private Methods	
//------------------------------------------------------------------------
	
	/**
	 * The method sends messages to the whole cluster via the 
	 * ClusterConnection. All cluster communication should be done 
	 * through this method. 
	 * 
	 * @param ClusterMSG
	 * @throws CMMExn_ClusterErr 
	 */
	private void SendClusterMessage(ClusterMessage ClusterMSG) throws CMMExn_ClusterErr{
		
		SA.AddLogEntry("CMM", "Info - Sending a message to the cluster. Message Type - " + ClusterMSG.getType());
		
		// Set this machine to be the sender machine 
		ClusterMSG.setSenderID(MachineDBLocalID);
		
		try {
			// Create a new SpreadMessage
			SpreadMessage Message = new SpreadMessage();
			Message.setObject(ClusterMSG);
			Message.addGroup(ClusterGroup);
			Message.setReliable();
			
			// Send the message to the cluster
			ClusterConnection.multicast(Message);
		} catch (SpreadException e) {
			// Report failure to send ClusterMessage
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to send a message to the cluster." + 
					" MessageType : " + ClusterMSG.getType());
			
			// Throw a Cluster Exception
			CMMExn_ClusterErr exn = new CMMExn_ClusterErr();
			throw exn;
		}
	}
	
//------------------------------------------------------------------------
//  Public Methods	
//------------------------------------------------------------------------
		
	
	public ClusterManagementModule(ServerApplication SA, ConnectionInformation myInfo) throws CMMExn_ClusterErr, UnknownHostException {
		
		this.SA = SA;
		privateName = "TestServer";
		SAConnInfo = myInfo;
		
		// Initialize the task database
		TaskDB = new TreeMap<Integer, TaskEntry>();
		
		// Initialize the machine database
		MachineDB = new TreeMap<Integer, MachineEntry>();
		
		SA.AddLogEntry("CMM", "Info - CMM Started.");
		SA.AddLogEntry("CMM", "Info - Trying to connect to the cluster via Spread");
		
		// Start Spread connection
		boolean priority = false;
		boolean groupMembership = false;
		
		try {
			// Open a new connection to the Spread
			ClusterConnection = new SpreadConnection();
			ClusterConnection.connect(null, 0, privateName, priority, groupMembership);
			
			// Create and add a new Spread listener for the spread
			SpreadListener = new AdvancedSpreadListener(this);
			ClusterConnection.add(SpreadListener);
			
			// Join the server group in the cluster
			ClusterGroup = new SpreadGroup();
			ClusterGroup.join(ClusterConnection, GroupName);
			
			
		} catch (SpreadException e) {
			// TODO Add report of failure to SA
			e.printStackTrace();
			SA.AddLogEntry("CMM", "Error - Failed to connect to the cluster. Not running CMM thread.");
			CMMExn_ClusterErr exn = new CMMExn_ClusterErr();
			throw exn;
//			e.printStackTrace();
//			return;
		}
		
		SA.AddLogEntry("CMM", "Info - Spread connection to the cluster established.");
		
		// TODO !!!Add training phase here!!!
		
		// TODO Remove this when training done
		synchronized(this){
			// Add local machine to the Machine database
			MachineDB.put(new Integer(0), new MachineEntry(0, SAConnInfo, MachineState.TRAINING));
			MachineDBLocalID = 0;
		}
		
		// TODO Replace with proxy selection and parameter transfer
		Proxy = new ProxyServer();
		
		// The CMM is now ready for steady state activity

		// Send message to the cluster - "New Server Ready"
		ClusterMessage_NewServerReady msg = new ClusterMessage_NewServerReady();
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			// TODO Add reporting to SA
			SA.AddLogEntry("CMM", "Error - Terminating CMM thread.");
		}
		
		// Create a new thread for the CMM
		// TODO implement a load balancing thread
//		CMMThread = new Thread(new ClusterManagementModule(SA, myInfo));
//		CMMThread.run();
		
		// TODO Continue...
	}

//------------------------------------------------------------------------	
	
	/* 
	 * This is the code of the CMM thread, which runs in a 
	 * different context.
	 * 
	 * (non-Javadoc)
	 * @see server.IClusterManagementModule#run()
	 */
	public void run(){
		// TODO Write CMM.run()

		SA.AddLogEntry("CMM", "Info - Started CMM thread.");
				
		// TODO Add steady state activity here
		// Here Be Dragons ...ok - maybe just load balancing and fault tolerance
	}

//	------------------------------------------------------------------------
//  ProxyServer Interface	
//	------------------------------------------------------------------------	
	
	public synchronized ConnectionInformation FindLeastBusyServer() {

		SA.AddLogEntry("CMM", "Info - Received a \"Find Least Busy Server\" request from the Proxy server.");
		
		// Transform the machine database from a TraaMap into a Set
		Set MachineSet = MachineDB.entrySet();
		// Create an Iterator to run through the Set
		Iterator it = MachineSet.iterator();
		int MinTaskAmount = Integer.MAX_VALUE;
		ConnectionInformation ConnInfo = null;
		Integer SelectedMachineID = null;
		// Run througn the Set
		while(it.hasNext()){
			// Retrieve an entry from the Set
			Map.Entry<Integer, MachineEntry> MapEntry = (Map.Entry<Integer, MachineEntry>)it.next();
			// Retrieve the machine entry from the Set entry
			MachineEntry MEntry = MapEntry.getValue();
			// if the machine's task amount is the current minimum
			if(MEntry.getTaskCount() <= MinTaskAmount){
				// Set task count minimum to this machine's task count 
				MinTaskAmount = MEntry.getTaskCount();
				// Take this machine's connection information for the return value
				ConnInfo = MEntry.getConnInfo();
				// Set MachineID 
				SelectedMachineID = MapEntry.getKey();
			}
		}
		
		SA.AddLogEntry("CMM", "Info - Chose least busy server - MachineID " + SelectedMachineID);
		
		return ConnInfo;
	}
	
//	------------------------------------------------------------------------
//  ServerApplication Interface	
//	------------------------------------------------------------------------	

	public void NewTask(SATaskContext Context) throws CMMExn_ClusterErr {

		SA.AddLogEntry("CMM", "Info - Recieved a \"New Task\" request from the SA.");
		
		// Create a "New Task" message 
		ClusterMessage_NewTask msg = new ClusterMessage_NewTask();
		msg.setTaskContext(Context);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"New Task\" message to the cluster.");
			throw e;
		}
	}

//------------------------------------------------------------------------	
	
	public void TaskContextUpdate(Integer TaskID,
			SAIncramentalTaskContext ContextChange) throws CMMExn_TaskErr, CMMExn_ClusterErr {

		SA.AddLogEntry("CMM", "Info - Recieved a \"Task Context Update\" request from the SA for TaskID " + TaskID);
		
		// This past is synchronized because 
		// access to CMM databases is restricted 
		synchronized(this){
			// Check if task exists in task database
			if(!TaskDB.containsKey(TaskID)){
				SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
		
			// Check if task belongs to local machine
			TaskEntry Task = TaskDB.get(TaskID);
			if(Task.getAssignedServerID() != MachineDBLocalID){
				SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + Task.getAssignedServerID() 
						+ ") does not match local MachineID (" + MachineDBLocalID +")");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
		}
		
		// Create an "Update Task" message
		ClusterMessage_UpdateTask msg = new ClusterMessage_UpdateTask();
		msg.setTaskID(TaskID);
		msg.setContextUpdate(ContextChange);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"Update Task\" message to the cluster." +
					" TaskID : " + TaskID);
			throw e;
		}
	}

//------------------------------------------------------------------------	
	
	public void TaskFinish(Integer TaskID) throws CMMExn_TaskErr, CMMExn_ClusterErr {
		
		SA.AddLogEntry("CMM", "Info - Recieved a \"Task Finish\" request from the SA for TaskID " + TaskID);
		
		// This past is synchronized because 
		// access to CMM databases is restricted 
		synchronized(this){
			// Check if task exists in task database
			if(!TaskDB.containsKey(TaskID)){
				SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
		
			// Check if task belongs to local machine
			TaskEntry Task = TaskDB.get(TaskID);
			if(Task.getAssignedServerID() != MachineDBLocalID){
				SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + Task.getAssignedServerID() 
					+ ") does not match local MachineID (" + MachineDBLocalID +")");
				CMMExn_TaskErr TaskErr = new CMMExn_TaskErr();
				throw TaskErr;
			}
		}
		
		// Create a "Terminate Task" message
		ClusterMessage_TerminateTask msg = new ClusterMessage_TerminateTask();
		msg.setTaskID(TaskID);
		
		// Send the message through the cluster
		try {
			SendClusterMessage(msg);
		} catch (CMMExn_ClusterErr e) {
			SA.AddLogEntry("CMM", "Error - Failed to send \"Terminate Task\" message to the cluster." +
					" TaskID : " + TaskID);
			throw e;
		}
	}

//	------------------------------------------------------------------------	
	
	public void Close() throws CMMExn_ClusterErr {
	
		try {
			SA.AddLogEntry("CMM", "Info - Received a \"Close\" instruction from the SA.");
			
			// Remove listener from the connection
			ClusterConnection.remove(SpreadListener);
			// Leave the Cluster group
			ClusterGroup.leave();
			// Disconnect from the Spread connection
			ClusterConnection.disconnect();
			SA.AddLogEntry("CMM", "Info - Disconnected Spread connection to the Cluster.");
			
			// TODO Add ProxyServer termination
			// TODO Add CMM tread termination
			
		} catch (SpreadException e) {
			SA.AddLogEntry("CMM", "Error - Failed to close the Spread connection to the Cluster.");
			CMMExn_ClusterErr exn = new CMMExn_ClusterErr();
			throw exn;
		}
	}
	
//	------------------------------------------------------------------------
//  AdvancedSpreadListener Interface	
//	------------------------------------------------------------------------	
	
	public synchronized void RecieveNewTask(Integer MachineID, TaskContext Context) {
	
		SA.AddLogEntry("CMM", "Info - Recieved a \"New Task\" message from the cluster." +
				" Sender ID - " + MachineID);
		
		// Verify that the machine ID exists in the machine database
		if(!MachineDB.containsKey(MachineID))
		{
			SA.AddLogEntry("CMM", "Error - MachineID " + MachineID + " does not exist in the Machine Database.");
			// TODO Add report to SA
			return;
		}
	
		// Retrieve previous maximum task ID
		Integer MaxTaskID;
		if(TaskDB.isEmpty())
			MaxTaskID = new Integer(-1);
		else
			MaxTaskID = TaskDB.lastKey();
	
		// Create a new task ID
		// New task ID is prev task ID incramented by 1
		Integer NewTaskID = new Integer(MaxTaskID.intValue()+1);
	
		// Create a new task entry
		TaskEntry NewEntry = new TaskEntry(MachineID, Context);
	
		// Add the new task entry to the task database
		TaskDB.put(NewTaskID, NewEntry);
	
		// Update the task amount entry of the relevant machine in
		// the machine database
		// Retrieve prev machine entry
		MachineEntry MEntry = MachineDB.get(MachineID);
		// Incrament the task count 
		MEntry.incTaskCount();
		// Put the machine entry back to the machine database
		// TODO CHECK if this is neccessery!! provably not, as MEntry is a reference into the DB
//		MachineDB.put(MachineID, MEntry);
		
		// If message originated on this machine
		if(MachineID.equals(MachineDBLocalID))
			try {
				SA.ReceiveNewTaskId(NewTaskID);
			} catch (SAExn_UnexpectedTaskId e) {
				// TODO Add task termination
				e.printStackTrace();
			} catch (SAExn_TaskIdAlreadyPresent e) {
				// TODO Add task termination
				e.printStackTrace();
			}
	}

//	------------------------------------------------------------------------	
	
	public synchronized void RecieveUpdateTask(Integer MachineID, Integer TaskID, SAIncramentalTaskContext ContextUpdate) {

		SA.AddLogEntry("CMM", "Info - Recieved an \"Update Task\" message from the cluster." +
				" Sender ID - " + MachineID + " , TaskID - " + TaskID);
		
		if(!MachineDB.containsKey(MachineID)){
			SA.AddLogEntry("CMM", "Error - Sender MachineID " + MachineID + " does not exist in the Machine Database.");
			// TODO add report to SA
			return;
		}
		if(!TaskDB.containsKey(TaskID)){
			SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
			// TODO add report to SA
			return;
		}
			
		// Retrieve task entry from task database
		TaskEntry TEntry = TaskDB.get(TaskID);
		// Verify that task belongs to the correct machine
		if(!TEntry.getAssignedServerID().equals(MachineID)){
			SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + TEntry.getAssignedServerID() 
					+ ") does not match sender MachineID (" + MachineID +")");
			// TODO add report to SA
			return;
		}
		// Retrieve task context from task entry
		TaskContext Context = TEntry.getContext();
		// Verify that task context type is APPLICATION
		if(Context.getType()!=TaskType.APPLICATION){
			SA.AddLogEntry("CMM", "Error - Task type is " + 
					Context.getType() + " instead of APPLICATION.");
			// TODO add report to SA
			return;
		}
		// Convert to SA task context
		SATaskContext OldSAContext = (SATaskContext)Context;
		// Reconstruct task's context from old context and incramental change
		SATaskContext NewSAContext = SA.ReconstructTaskContext(OldSAContext, ContextUpdate);
		// Put reconstructed context into the task's entry
		TEntry.putContext(NewSAContext);
			
		// TODO CHECK if this is neccesery! provably not, as TEntry is a reference into the DB
//		TaskDB.put(TaskID, TEntry);
		
	}

//	------------------------------------------------------------------------	
	
	public synchronized void RecieveTerminateTask(Integer MachineID, Integer TaskID) {

		SA.AddLogEntry("CMM", "Info - Recieved a \"Terminate Task\" message from the cluster." +
				" Sender ID - " + MachineID + " , TaskID - " + TaskID);

		if(!MachineDB.containsKey(MachineID)){
			SA.AddLogEntry("CMM", "Error - Sender MachineID " + MachineID + " does not exist in the Machine Database.");
			// TODO add report to SA
			return;
		}
		if(!TaskDB.containsKey(TaskID)){
			SA.AddLogEntry("CMM", "Error - TaskID " + TaskID + " does not exist in the Task Database.");
			// TODO add report to SA
			return;
		}
			
		// Retrieve task's entry from the task database
		TaskEntry TEntry = TaskDB.get(TaskID);
		// Verify that the task belongs to the correct machine
		if(!TEntry.getAssignedServerID().equals(MachineID)){
			SA.AddLogEntry("CMM", "Error - Task's assigned Server (" + TEntry.getAssignedServerID() 
					+ ") does not match sender MachineID (" + MachineID +")");
			// TODO add report to SA
			return;
		}
		// Remove task's entry from the task database
		TaskDB.remove(TaskID);
		// Retrieve machine's entry from the machine database
		MachineEntry MEntry = MachineDB.get(MachineID);
		// Decrament the task count of the machine
		MEntry.decTaskCount();
		// TODO CHECK if this is neccesery! provably not, as MEntry is a reference into the DB 
//		MachineDB.put(MachineID, MEntry);			
		
	}
	
//	------------------------------------------------------------------------	

	public synchronized void RecieveNewServerReady(Integer MachineID) {

		SA.AddLogEntry("CMM", "Info - Recieved a \"New Server Ready\" message from the cluster." +
				" Sender ID - " + MachineID);

		if(!MachineDB.containsKey(MachineID)){
			SA.AddLogEntry("CMM", "Error - Sender MachineID " + MachineID + " does not exist in the Machine Database.");
			// TODO add report to SA
			return;
		}
			
		// Retrieve the machine entry from the machine database
		MachineEntry MEntry = MachineDB.get(MachineID);
		// Verify that the machine is in training
		if(MEntry.getMachineState() != MachineState.TRAINING){
			SA.AddLogEntry("CMM", "Error - Task type is " + 
					MEntry.getMachineState() + " instead of TRAINING.");
			// TODO add report to SA
			return;
		}
		// Change machine status from TRAINING to ACTIVE
		MEntry.putMachineState(MachineState.ACTIVE);
		// TODO CHECK if need to add commit back to the DB
	}

//	------------------------------------------------------------------------	
	
	public synchronized void RecieveUnsupportedMessage() {
		// TODO Auto-generated method stub
		SA.AddLogEntry("CMM", "Error - Received an unsupported message from the cluster.");
		// TODO add report to SA
	}

}
